﻿using System;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.EditorInput;
using System.Diagnostics;
using System.Collections.Generic;
using System.Data;
using System.Linq;

// This line is not mandatory, but improves loading performances
[assembly: CommandClass(typeof(AcadToAnsysDotNet.MyCommands))]

namespace AcadToAnsysDotNet
{

    // This class is instantiated by AutoCAD for each document when
    // a command is called by the user the first time in the context
    // of a given document. In other words, non static data in this class
    // is implicitly per-document!
    public class MyCommands
    {
        #region private fields
        private Point3d _defaultReferencePoint = new Point3d(0, 0, 0);
        private double _defaultScaleRatio = 100.0;
        private double _defaultTolerance = 1.0e-2;
        #endregion

        #region Demo commands
        // The CommandMethod attribute can be applied to any public  member 
        // function of any public class.
        // The function should take no arguments and return nothing.
        // If the method is an intance member then the enclosing class is 
        // intantiated for each document. If the member is a static member then
        // the enclosing class is NOT intantiated.
        //
        // NOTE: CommandMethod has overloads where you can provide helpid and
        // context menu.

        // Modal Command with localized name
        [CommandMethod("MyGroup", "MyCommand", "MyCommandLocal", CommandFlags.Modal)]
        public void MyCommand() // This method can have any name
        {
            Document doc = Application.DocumentManager.MdiActiveDocument;
            if (doc != null)
            {
                var ed = doc.Editor;
                var db = ed.Document.Database;
                Debug.Assert(ed != null && db != null);

                var circle = new Circle(new Point3d(100, 100, 0), Vector3d.ZAxis, 200);
                AppendEntityToDatabase(db, circle);

                var line = new Line(new Point3d(100, 100, 0), new Point3d(300, 300, 0));
                var lineId = AppendEntityToDatabase(db, line);

                using (var trans = db.TransactionManager.StartTransaction())
                {
                    var newline = trans.GetObject(lineId, OpenMode.ForWrite);
                    newline.Erase();
                    trans.Commit();
                }
            }
        }

        [CommandMethod("MyGroup", "MyPickFirst", "MyPickFirstLocal", CommandFlags.Modal | CommandFlags.UsePickSet)]
        public void MyPickFirst() // This method can have any name
        {
            PromptSelectionResult result = Application.DocumentManager.MdiActiveDocument.Editor.GetSelection();
            if (result.Status == PromptStatus.OK)
            {

            }
            else
            {
                // There are no selected entities
                // Put your command code here
            }
        }

        // Application Session Command with localized name
        [CommandMethod("MyGroup", "MySessionCmd", "MySessionCmdLocal", CommandFlags.Modal | CommandFlags.Session)]
        public void MySessionCmd()
        {
            // Put your command code here
        }

        // LispFunction is similar to CommandMethod but it creates a lisp 
        // callable function. Many return types are supported not just string
        // or integer.
        [LispFunction("MyLispFunction", "MyLispFunctionLocal")]
        public int MyLispFunction(ResultBuffer args)
        {
            // Put your command code here

            // Return a value to the AutoCAD Lisp Interpreter
            return 1;
        }
        #endregion

        /// <summary>
        /// Export curve to ANSYS
        /// </summary>
        [CommandMethod("MyGroup", "ATA", "ATA", CommandFlags.Modal | CommandFlags.UsePickSet)]
        public void ExportToAnsys()
        {
            var doc = Application.DocumentManager.MdiActiveDocument;
            Debug.Assert(doc != null);
            var ed = doc.Editor;
            var db = doc.Database;
            Debug.Assert(ed != null && db != null);

            //
            // Get selected objectIds
            //
            var selectionResult = ed.GetSelection();
            if (selectionResult.Status != PromptStatus.OK)
                return;
            var allSelectedIds = selectionResult.Value.GetObjectIds();
            if (allSelectedIds.Length <= 0)
                return;

            //
            // Set the global tolerance
            //
            var defaultGlobalTolerance = Tolerance.Global;
            Tolerance.Global = new Tolerance(_defaultTolerance, _defaultTolerance);

            //
            // Read the selected curves
            //
            var selectedCurves = new List<ExportCurve>();
            using (var trans = db.TransactionManager.StartTransaction())
            {
                foreach (var id in allSelectedIds)
                {
                    var curve = trans.GetObject(id, OpenMode.ForRead) as Curve;
                    Debug.Assert(curve != null);
                    if (curve is Line)
                    {
                        var line = curve as Line;
                        Debug.Assert(line != null);
                        selectedCurves.Add(new ExportCurve()
                            {
                                ObjectId = id,
                                CurveType = ExportCurveType.Line,
                                StartNode = new ExportNode() { NodeId = -1, Location = line.StartPoint },
                                EndNode = new ExportNode() { NodeId = -1, Location = line.EndPoint }
                            });
                    }
                }
            }

            var curveNodes = new List<ExportNode>();
            int newNodeId = 0;
            foreach (var curve in selectedCurves)
            {
                var existingNode = curveNodes.Find(node => node.IsSameLocationWith(curve.StartNode));
                if (existingNode != null)
                {
                    Debug.Assert(existingNode.NodeId >= 0);
                    curve.StartNode = existingNode;
                }
                else
                {
                    curve.StartNode.NodeId = newNodeId++;
                    curveNodes.Add(curve.StartNode);
                }

                existingNode = curveNodes.Find(node => node.IsSameLocationWith(curve.EndNode));
                if (existingNode != null)
                {
                    Debug.Assert(existingNode.NodeId >= 0);
                    curve.EndNode = existingNode;
                }
                else
                {
                    curve.EndNode.NodeId = newNodeId++;
                    curveNodes.Add(curve.EndNode);
                }
            }

            //
            // Get all of the intersection points
            //
            var exportNodes = new List<ExportNode>(curveNodes);
            var exportCurves = new List<ExportCurve>();
            using (var trans = db.TransactionManager.StartTransaction())
            {
                foreach (var thisCurve in selectedCurves)
                {
                    var allIntersectPoints = new List<Point3d>();
                    var thisCurveEntity = trans.GetObject(thisCurve.ObjectId, OpenMode.ForRead) as Curve;
                    Debug.Assert(thisCurveEntity != null);
                    foreach (var otherCurve in selectedCurves)
                    {
                        if (otherCurve.IsSameCurveWith(thisCurve))
                            continue;

                        var otherCurveEntity = trans.GetObject(otherCurve.ObjectId, OpenMode.ForRead) as Curve;
                        Debug.Assert(otherCurveEntity != null);
                        var interPts = new Point3dCollection();
                        thisCurveEntity.IntersectWith(otherCurveEntity, Intersect.OnBothOperands,
                            interPts, IntPtr.Zero, IntPtr.Zero);
                        var projectPlane = new Plane(new Point3d(0, 0, 0), new Vector3d(0, 0, 1));
                        thisCurveEntity.IntersectWith(otherCurveEntity, Intersect.OnBothOperands,
                            projectPlane, interPts, IntPtr.Zero, IntPtr.Zero);
                        foreach (var pt in interPts)
                        {
                            allIntersectPoints.Add((Point3d)pt);
                        }
                    }

                    var refinedIntersectPoints = allIntersectPoints
                        .Where(pt => (!IsSameLocation(pt, thisCurveEntity.StartPoint))
                            && (!IsSameLocation(pt, thisCurveEntity.EndPoint))
                            && IsPointOnCurve(thisCurveEntity, pt))
                        .OrderBy(pt => thisCurveEntity.GetParameterAtPoint(pt)).ToList();
                    refinedIntersectPoints.Insert(0, thisCurveEntity.StartPoint);
                    refinedIntersectPoints.Add(thisCurveEntity.EndPoint);
                    for (int i = 0; i < refinedIntersectPoints.Count - 1; i++)
                    {
                        var startPoint = refinedIntersectPoints[i];
                        var startNode = new ExportNode() { NodeId = -1, Location = startPoint };
                        var existingNode = exportNodes.Find(node => node.IsSameLocationWith(startNode));
                        if (existingNode != null)
                        {
                            Debug.Assert(existingNode.NodeId >= 0);
                            startNode = existingNode;
                        }
                        else
                        {
                            startNode.NodeId = newNodeId++;
                            exportNodes.Add(startNode);
                        }

                        var endPoint = refinedIntersectPoints[i + 1];
                        var endNode = new ExportNode() { NodeId = -1, Location = endPoint };
                        existingNode = exportNodes.Find(node => node.IsSameLocationWith(endNode));
                        if (existingNode != null)
                        {
                            Debug.Assert(existingNode.NodeId >= 0);
                            endNode = existingNode;
                        }
                        else
                        {
                            endNode.NodeId = newNodeId++;
                            exportNodes.Add(endNode);
                        }

                        exportCurves.Add(new ExportCurve()
                        {
                            ObjectId = thisCurve.ObjectId,
                            CurveType = thisCurve.CurveType,
                            StartNode = startNode,
                            EndNode = endNode
                        });
                    }
                }
            }

            var curveEqualityComparer = new ExportCurveEqualityComparer();
            exportCurves = exportCurves.Where(curve => !curve.StartNode.IsSameNodeWith(curve.EndNode)).Distinct(curveEqualityComparer).ToList();

            // 
            // Generate ANSYS command stream
            //
            string splitString = "! ************************************************************************";
            var curTime = DateTime.Now;
            var ansysCmdLines = new List<string>() 
            {
                splitString,
                "! **** Generated by AutocadToAnsys v2014.11.28 (Author: wudong <wdhust@gmail.com>)",
                string.Format("! ****        at <{0} {1}>", curTime.ToShortDateString(),curTime.ToLongTimeString()),
                "/prep7",
                "/uis,msgpop,3",
                "lsel,none,all",
                "*get,kst_,kp,0,num,max   ! Get max keypoint number",
                "kst_=kst_+1"
            };

            // Generate key points
            Debug.Assert(exportNodes.All(node => node.NodeId >= 0));
            Debug.Assert(exportNodes.Distinct(new NodeEqualityComparer()).Count() == exportNodes.Count);
            double xmin = double.MaxValue, xmax = double.MinValue; 
            double ymin = double.MaxValue, ymax = double.MinValue;
            for (int i = 0; i < exportNodes.Count; i++)
            {
                int nodeNum = exportNodes[i].NodeId;
                double x = (exportNodes[i].Location.X - _defaultReferencePoint.X) / _defaultScaleRatio;
                double y = (exportNodes[i].Location.Y - _defaultReferencePoint.Y) / _defaultScaleRatio;
                ansysCmdLines.Add(string.Format("k,kst_+{0},{1:F5},{2:F5}", nodeNum, x, y));

                if (x < xmin) xmin = x;
                if (x > xmax) xmax = x;
                if (y < ymin) ymin = y;
                if (y > ymax) ymax = y;
            }

            // Generate curves
            foreach (var curve in exportCurves)
            {
                int startNodeNum = curve.StartNode.NodeId;
                int endNodeNum = curve.EndNode.NodeId;
                Debug.Assert(startNodeNum >= 0 && endNodeNum >= 0 && startNodeNum != endNodeNum);
                ansysCmdLines.Add(string.Format("lstr,kst_+{0},kst_+{1}", startNodeNum, endNodeNum));
            }

            //
            ansysCmdLines.AddRange(new string[]
            {
                "kst_=",
                "/uis,msgpop,2",
                string.Format("x_min_ = {0:F5}", xmin),
                string.Format("x_max_ = {0:F5}", xmax),
                string.Format("y_min_ = {0:F5}", ymin),
                string.Format("y_max_ = {0:F5}", ymax),
                splitString,
                ""
            });

            var cmdString = string.Join(Environment.NewLine, ansysCmdLines);
            System.Windows.Clipboard.SetDataObject(cmdString);
            ed.WriteMessage("\n导出的ANSYS命令流已复制到剪贴板中!");

            //
            // Restore to default tolerance setting
            //
            Tolerance.Global = defaultGlobalTolerance;
        }

        /// <summary>
        /// Query the range of a specified selection region
        /// </summary>
        [CommandMethod("MyGroup", "ATR", "ATR", CommandFlags.Modal)]
        public void QuerySelRange()
        {
            var doc = Application.DocumentManager.MdiActiveDocument;
            Debug.Assert(doc != null);
            var ed = doc.Editor;
            var db = doc.Database;
            Debug.Assert(ed != null && db != null);
            var pointResult = ed.GetPoint(new PromptPointOptions("\n选择查询范围的第一个点: ")
            {
                UseBasePoint = true,
                BasePoint = this._defaultReferencePoint
            });
            if (pointResult.Status != PromptStatus.OK)
                return;
            var queryPt1 = pointResult.Value;

            pointResult = ed.GetPoint(new PromptPointOptions("\n选择查询范围的第二个点: ")
            {
                UseBasePoint = true,
                BasePoint = this._defaultReferencePoint
            });
            if (pointResult.Status != PromptStatus.OK)
                return;
            var queryPt2 = pointResult.Value;

            var refPt = this._defaultReferencePoint;
            var scaleRatio = this._defaultScaleRatio;
            var queryResult = string.Format("\nsel,s,loc,x,{0:F3},{1:F3}",
                (queryPt1.X - refPt.X) / scaleRatio, (queryPt2.X - refPt.X) / scaleRatio)
                + string.Format("\nsel,r,loc,y,{0:F3},{1:F3}",
                (queryPt1.Y - refPt.Y) / scaleRatio, (queryPt2.Y - refPt.Y) / scaleRatio);
            System.Windows.Clipboard.SetDataObject(queryResult);
            ed.WriteMessage("\n范围查询结果(已复制到剪贴板): " + queryResult);
        }

        /// <summary>
        /// Query the coordinate of a specified point
        /// </summary>
        [CommandMethod("MyGroup", "ATC", "ATC", CommandFlags.Modal)]
        public void QueryPointCoordinate()
        {
            var doc = Application.DocumentManager.MdiActiveDocument;
            Debug.Assert(doc != null);
            var ed = doc.Editor;
            var db = doc.Database;
            Debug.Assert(ed != null && db != null);
            var pointResult = ed.GetPoint(new PromptPointOptions("\n选择需查询坐标的点: ")
            {
                UseBasePoint = true,
                BasePoint = this._defaultReferencePoint
            });
            if (pointResult.Status != PromptStatus.OK)
                return;
            var queryPt = pointResult.Value;
            var refPt = this._defaultReferencePoint;
            var scaleRatio = this._defaultScaleRatio;
            ed.WriteMessage("\n{0:F3},{1:F3}", 
                (queryPt.X - refPt.X) / scaleRatio, (queryPt.Y - refPt.Y) / scaleRatio);
        }

        /// <summary>
        /// Set the global options of AutocadToAnsys
        /// </summary>
        [CommandMethod("MyGroup", "ATS", "ATS", CommandFlags.Modal)]
        public void SetOptions()
        {
            var doc = Application.DocumentManager.MdiActiveDocument;
            Debug.Assert(doc != null);
            var ed = doc.Editor;
            var db = doc.Database;
            Debug.Assert(ed != null && db != null);

            //
            // set reference point
            //
            var pointResult = ed.GetPoint(new PromptPointOptions(string.Format("\n选择原点: <{0}>",
                GetPoint3dFormatString(this._defaultReferencePoint)))
            {
                AllowNone = true
            });
            if ((pointResult.Status == PromptStatus.OK || pointResult.Status == PromptStatus.None) == false)
                return;
            if (pointResult.Status == PromptStatus.OK)
                this._defaultReferencePoint = pointResult.Value;

            //
            // set scale ratio
            //
            var scaleRatioResult = ed.GetDouble(new PromptDoubleOptions(string.Format("\n输入缩放比例系数: "))
            {
                DefaultValue = this._defaultScaleRatio,
                AllowNegative = false,
                AllowZero = false,
                AllowNone = true
            });
            if ((scaleRatioResult.Status == PromptStatus.OK || scaleRatioResult.Status == PromptStatus.None) == false)
                return;
            if (scaleRatioResult.Status == PromptStatus.OK)
                _defaultScaleRatio = scaleRatioResult.Value;

            //
            // set tolerance
            //
            var toleranceResult = ed.GetDouble(new PromptDoubleOptions(string.Format("\n输入容差值: "))
            {
                DefaultValue = this._defaultTolerance,
                AllowNegative = false,
                AllowZero = false,
                AllowNone = true
            });
            if ((toleranceResult.Status == PromptStatus.OK || toleranceResult.Status == PromptStatus.None) == false)
                return;
            if (toleranceResult.Status == PromptStatus.OK)
                _defaultTolerance = toleranceResult.Value;
        }

        #region helper functions
        ObjectId AppendEntityToDatabase(Database db, Entity entity)
        {
            ObjectId id = ObjectId.Null;
            using (var trans = db.TransactionManager.StartTransaction())
            {
                var bt = trans.GetObject(db.CurrentSpaceId, OpenMode.ForWrite) as BlockTableRecord;
                Debug.Assert(bt != null);
                id = bt.AppendEntity(entity);
                trans.AddNewlyCreatedDBObject(entity, true);
                trans.Commit();
            }
            return id;
        }

        private static bool IsPointOnCurve(Curve cv, Point3d pt)
        {
            try
            {
                Point3d p = cv.GetClosestPointTo(pt, false);
                return (p - pt).Length <= Tolerance.Global.EqualPoint;
            }
            catch { }
            return false;
        }

        private class ExportNode
        {
            public int NodeId;
            public Point3d Location;

            public override string ToString()
            {
                return string.Format("{0}_{1}", this.NodeId, this.Location.ToString());
            }

            public bool IsSameNodeWith(ExportNode otherNode)
            {
                Debug.Assert(this.NodeId >= 0 && otherNode.NodeId >= 0);
                if (this.NodeId == otherNode.NodeId)
                {
                    Debug.Assert(PointInPlaneDistance(this.Location, otherNode.Location) < Tolerance.Global.EqualPoint);
                    return true;
                }
                else
                {
                    return false;
                }
            }

            public bool IsSameLocationWith(ExportNode otherNode)
            {
                return PointInPlaneDistance(this.Location, otherNode.Location) < Tolerance.Global.EqualPoint;
            }
        }

        private class NodeEqualityComparer : IEqualityComparer<ExportNode>
        {
            bool IEqualityComparer<ExportNode>.Equals(ExportNode node1, ExportNode node2)
            {
                return node1.IsSameNodeWith(node2);
            }

            int IEqualityComparer<ExportNode>.GetHashCode(ExportNode node)
            {
                return node.NodeId;
            }
        }

        private enum ExportCurveType
        {
            Line,
            Arc,
            Circle
        }

        private class ExportCurve
        {
            public ObjectId ObjectId;
            public ExportCurveType CurveType;

            public ExportNode StartNode;
            public ExportNode EndNode;

            public bool IsSameCurveWith(ExportCurve otherCurve)
            {
                return (this.ObjectId == otherCurve.ObjectId);
            }
        }

        private class ExportCurveEqualityComparer : IEqualityComparer<ExportCurve>
        {

            bool IEqualityComparer<ExportCurve>.Equals(ExportCurve curve1, ExportCurve curve2)
            {
                if (curve1.CurveType == curve2.CurveType)
                {
                    Debug.Assert(curve1.CurveType == ExportCurveType.Line);     // Only the line curve is considered at present
                    return (curve1.StartNode.IsSameNodeWith(curve2.StartNode) && curve1.EndNode.IsSameNodeWith(curve2.EndNode))
                        || (curve1.StartNode.IsSameNodeWith(curve2.EndNode) && curve1.EndNode.IsSameNodeWith(curve2.StartNode));
                }
                else
                {
                    return false;
                }
            }

            int IEqualityComparer<ExportCurve>.GetHashCode(ExportCurve curve)
            {
                return string.Format("{0}_{1}_{2}", curve.CurveType.ToString(),
                    curve.StartNode.ToString(), curve.EndNode.ToString()).GetHashCode();
            }
        }

        public static bool IsSameLocation(Point3d pt1, Point3d pt2)
        {
            return PointInPlaneDistance(pt1, pt2) < Tolerance.Global.EqualPoint;

        }

        public static double PointInPlaneDistance(Point3d pt1, Point3d pt2)
        {
            double dx = pt1.X - pt2.X;
            double dy = pt1.Y - pt2.Y;
            return Math.Sqrt(dx * dx + dy * dy);
        }

        public static string GetPoint3dFormatString(Point3d pt)
        {
            return string.Format("({0:F3},{1:F3},{2:F3})", pt.X, pt.Y, pt.Z);
        }
        #endregion
    }

}
